Channel,即通道,衍生自Charles Antony Richard Hoare的CSP併發模型,在Go語言中具有極其重要的地位。雖然它可用於同步記憶體的訪問,但更適合用於goroutine之間傳遞資訊。就像我們在之前的“Go的並發哲學”章節所提到的那樣,通道在任何規模的程序編碼中都非常有用,因為它足夠靈活,能夠以各種方式組合在一起。
以下是一個channel的基本例子
func main() {
stringStream := make(chan string)
go func() {
stringStream <- "Hello channels!"
}()
salutation, ok := <-stringStream // <1>
fmt.Printf("(%v): %v", ok, salutation)
}
輸出:(true): Hello channels!
stringStream := make(chan string): 這裡創建了一個名為 stringStream 的 channel,專用於傳遞 string 類型的資料。
go func() {...}(): 這裡使用 go 關鍵字啟動了一個新的 goroutine。在這個 goroutine 中,程式向 stringStream channel 發送了一條消息 "Hello channels!"。
salutation, ok := <-stringStream: 這裡,主 goroutine 從 stringStream channel 中接收消息。接收操作會返回兩個值:
第一個值 (salutation) 是從 channel 中接收到的資料。
第二個值 (ok) 是一個布林值,表示接收操作是否成功。如果 channel 已被關閉且沒有其他消息,則這個值會是 false;否則為 true。
fmt.Printf("(%v): %v", ok, salutation): 最後,主 goroutine 會輸出接收到的消息和接收操作的結果 (即 ok 的值)。
接著是關閉channel並且讀取的例子,可以看到這邊的ok會是false,表示channel沒有未讀取的消息。
func main() {
// 創建一個整數型態的 channel
intStream := make(chan int)
// 關閉這個 channel
close(intStream)
// 從已經關閉的 channel 讀取資料。這裡會立即返回兩個值:
// 第一個值 (integer) 是預設的零值 (對於 int 類型來說是 0),
// 第二個值 (ok) 是一個布林值,表示 channel 是否仍有未讀取的消息。
integer, ok := <-intStream // <1>
// 輸出這兩個返回的值
fmt.Printf("(%v): %v", ok, integer)
}
這個例子是要說即使是已經關閉的channel,還是可以從裡面讀取值。
package main
import (
"fmt"
)
func main() {
// 創建一個整數型態的 channel
ch := make(chan int, 3)
// 向 channel 發送一些整數
ch <- 1
ch <- 2
ch <- 3
// 關閉 channel
close(ch)
// 從已經關閉的 channel 中讀取資料直到 channel 被耗盡
for integer := range ch {
fmt.Println(integer)
}
}
這個圖片很清楚的告訴我們在channel的各種狀態下進行操作會產生的情況:
當一個通道處於 nil 狀態時,任何試圖在該通道上進行的發送或接收都會被阻塞。當一個通道處於開啟狀態時,可以發送和接收信號。當一個通道被置於關閉狀態時,信號不能再被發送,但仍然可以接收信號。
var ch chan int // 在這裡,ch如果沒有被初始化,默認就是 nil
關閉某個通道同樣可以被作為向多個goroutine同時發生消息的方式之一。如果你有多個goroutine在單個 通道上等待,你可以簡單的關閉通道,而不是循環解除每一個goroutine的阻塞。
func main() {
// 創建一個名為 'begin' 的通道,用於同步 goroutines 的開始時機。
begin := make(chan interface{})
var wg sync.WaitGroup
// 啟動五個 goroutines。
for i := 0; i < 5; i++ {
wg.Add(1) // 增加等待組的計數器。
go func(i int) {
defer wg.Done() // 完成後,減少等待組的計數器。
<-begin // 等待 'begin' 通道被關閉。
fmt.Printf("%v has begun\n", i)
}(i)
}
// 輸出提示信息。
fmt.Println("Unblocking goroutines...")
// 關閉 'begin' 通道,這會解除上面的五個 goroutines 的阻塞。
close(begin)
wg.Wait() // 等待所有的 goroutines 完成。
}
當一個通道被關閉時,從該通道接收的所有操作都會立即完成,而不再阻塞。接收到的值將是通道元素類型的零值。此外,關於已關閉的通道,任何後續的接收操作都可以立即進行,而無需等待。
每個 goroutine 都在 <-begin 這裡阻塞,等待從 begin 通道接收一個值。但當 begin 通道被關閉時,這些正在等待的 goroutines 會立即從 <-begin 這裡返回,因為接收操作立即完成,並且接收到的是零值(在這種情況下是 nil)。
最後是介紹channel的buffer(緩衝),如果通道沒有緩衝,生產者每發送一個整數都會被阻塞,直到主 goroutine 接收到該整數。這會使生產者和消費者更緊密地同步,而緩衝的存在則允許一定程度的非阻塞交互。
但我覺得書上的例子沒有很明顯表達出緩衝與否的情況,所以我用一個比較簡單的對比範例來說明。
package main
import (
"fmt"
"time"
)
func main() {
// 無緩衝的通道
unbuffered := make(chan string)
go func() {
unbuffered <- "unbuffered"
fmt.Println("Sent to unbuffered channel!")
}()
time.Sleep(time.Second * 1) // 故意延遲
fmt.Println(<-unbuffered) // 取出值後,goroutine才會繼續運行
}
對於無緩衝的通道,goroutine 會在嘗試發送數據到通道時阻塞,直到主函數讀取通道的數據。因此,你會先看到 "Sent to unbuffered channel!" 被打印,然後再是 "unbuffered"。
package main
import (
"fmt"
"time"
)
func main() {
// 帶緩衝的通道
buffered := make(chan string, 1) // 緩衝大小為1
go func() {
buffered <- "buffered"
fmt.Println("Sent to buffered channel!")
}()
time.Sleep(time.Second * 1)
fmt.Println(<-buffered)
}
對於帶有緩衝的通道,由於緩衝的大小為1,goroutine 可以立即將數據發送到通道而不阻塞。所以,你會立即看到 "Sent to buffered channel!",然後是 "buffered"。